﻿using Microsoft.CSharp;
using OfficeOpenXml;
using OfficeOpenXml.Style;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Serialization;

namespace Experimentor
{
    public partial class form_ExperimRun : Form
    {
        private bool isPreview;
        private string currentFile;
        private ExperimentDef currentDef;
        private ExcelPackage currentExcel;
        private string currentSession;
        private Assembly assUserdefFunctions;
        private DatasaveObject datasaveObject;
        private DatasaveObject setData;

        private MainWorker mainWorker;

        private SequenceWorker curSequenceWorker;
        private Sequence curSequence;
        
        private List<int> curSetOrder;
        private SetWorker curSetWorker;
        private int curSet;
        private Dictionary<string, List<int>> presentationTimes;

        private List<SequencePart> curSeqParts;
        private SeqPartWorker curSeqPartWorker;

        private char[] alphabet = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };

        public form_ExperimRun()
        {
            InitializeComponent();
            this.isPreview = false;
            this.assUserdefFunctions = null;
            this.FormBorderStyle = FormBorderStyle.None;
            this.WindowState = FormWindowState.Maximized;
            this.Load += new EventHandler(this.form_ExperimRun_Load);
            this.presentationTimes = new Dictionary<string, List<int>>();
        }

        public form_ExperimRun(bool preview, string currentFile)
            : this()
        {
            this.isPreview = preview;
            this.currentFile = currentFile;
            this.currentDef = new ExperimentDef();
            this.Text = "Experimentor - Run Experiment - " + Path.GetFileNameWithoutExtension(this.currentFile);
            FileStream fs = new FileStream(this.currentFile, FileMode.Open);
            XmlSerializer xmls = new XmlSerializer(this.currentDef.GetType());
            try
            {
                this.currentDef = (ExperimentDef)(xmls.Deserialize(fs));
                this.currentSession = Guid.NewGuid().ToString("N");
                string excelfilename = Path.GetDirectoryName(this.currentFile) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(this.currentFile) + ".xlsx";
                this.currentExcel = new ExcelPackage(new FileInfo(excelfilename));
                this.datasaveObject = new DatasaveObject(this.isPreview, this.currentSession, Path.GetDirectoryName(this.currentFile), this.currentExcel);
                this.setData = new DatasaveObject(this.isPreview, this.currentSession, Path.GetDirectoryName(this.currentFile), this.currentExcel);
            }
            catch (InvalidOperationException ioe)
            {
                MessageBox.Show("Cannot open file: " + ioe.InnerException.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                this.currentDef = null;
            }
            fs.Close();
        }

        private void form_ExperimRun_Load(object sender, EventArgs e)
        {
            this.TopMost = true;
            this.mainWorker = new MainWorker(this.mainWorkerThread);
            this.mainWorker.RunWorkerAsync();
        }

        private void mainWorkerThread(object sender, DoWorkEventArgs e)
        {
            MainWorker worker = sender as MainWorker;
            if (this.currentDef != null && this.currentDef.userdefFunctions.Count>0)
            {
                string source = @"
namespace Experimentor
{
    public static class User_defined_functions
    {
        ";
                foreach (UserdefFunction func in this.currentDef.userdefFunctions)
                {
                    UserdefFunctionType type = func.getFunctiontypeInstance();
                    source += ("public static " + type.Returntype + " " + func.Name + "(" + type.Params + ")\n");
                    source += "{\n";
                    source += (func.Code + "\n");
                    source += "}\n\n";
                }

                source += @"
    }
}
";
                Dictionary<string, string> providerOptions = new Dictionary<string, string>
                {
                    {"CompilerVersion", "v3.5"}
                };
                CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);
                CompilerParameters compilerParams = new CompilerParameters
                {
                    GenerateInMemory = true,
                    GenerateExecutable = false
                };
                compilerParams.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location);
                compilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll");
                compilerParams.ReferencedAssemblies.Add("EPPlus.dll");
                CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, source);
                if (results.Errors.Count != 0)
                {
                    this.Invoke((MethodInvoker)delegate { MessageBox.Show(this, "FATAL: Incorrect user defined functions.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); });
                    this.Invoke((MethodInvoker)delegate { this.Close(); });
                }
                else this.assUserdefFunctions = results.CompiledAssembly;
            }
            if (this.currentDef != null && this.currentDef.personAttributes.Count>0)
            {
                this.Invoke((MethodInvoker)delegate { this.showAttributesForm(); });
                worker.mre.WaitOne();
                worker.mre.Reset();
            }
            if (this.currentDef != null && this.currentDef.sequences.Count > 0)
            {
                this.Invoke((MethodInvoker)delegate { this.doSequences(0); });
                worker.mre.WaitOne();
            }
            this.Invoke((MethodInvoker)delegate { this.Close(); });
        }

        private void showAttributesForm ()
        {
            List<PersonAttribute> attributes = this.currentDef.personAttributes.ToList<PersonAttribute>();
            if (attributes.Count == 0)
            {
                this.mainWorker.mre.Set();
                return;
            }
            attributes.Sort();
            Form attForm = new Form();
            attForm.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
            attForm.MinimizeBox = false;
            attForm.MaximizeBox = false;
            attForm.Text = "Bitte geben Sie ein ...";
            FlowLayoutPanel panel = new FlowLayoutPanel();
            panel.FlowDirection = FlowDirection.TopDown;
            panel.Size = new System.Drawing.Size(200, 10);
            panel.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly;
            panel.AutoSize = true;
            for (int i = 0; i < attributes.Count; i++)
            {
                FlowLayoutPanel innerPanel = new FlowLayoutPanel();
                innerPanel.Name = "panel_" + attributes[i].Varname;
                innerPanel.FlowDirection = FlowDirection.TopDown;
                innerPanel.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
                innerPanel.AutoSize = true;
                Label attText = new Label();
                attText.Name = "label_" + attributes[i].Varname;
                attText.Text = attributes[i].Text;
                innerPanel.Controls.Add(attText);
                string[] attValues;
                switch (attributes[i].Fieldtype)
                {
                    case PAFieldtype.Textfield:
                        TextBox attField = new TextBox();
                        attField.Name = attributes[i].Varname;
                        innerPanel.Controls.Add(attField);
                        break;
                    case PAFieldtype.Radiobuttons:
                        attValues = attributes[i].Values.Split(';');
                        foreach (string attValue in attValues)
                        {
                            string[] attKeyValue = attValue.Split('=');
                            RadioButton button = new RadioButton();
                            button.Name = attributes[i].Varname + "_" + attKeyValue[0];
                            button.Text = attKeyValue[1];
                            innerPanel.Controls.Add(button);
                        }
                        break;
                    case PAFieldtype.Selector:
                        attValues = attributes[i].Values.Split(';');
                        ComboBox combobox = new ComboBox();
                        combobox.Name = attributes[i].Varname;
                        DataTable dtKeyValue = new DataTable();
                        dtKeyValue.Columns.Add("Key",typeof(int));
                        dtKeyValue.Columns.Add("Value");
                        foreach (string attValue in attValues)
                        {
                            string[] attKeyValue = attValue.Split('=');
                            dtKeyValue.Rows.Add(Convert.ToInt32(attKeyValue[0]), attKeyValue[1]);
                        }
                        combobox.DataSource = dtKeyValue;
                        combobox.DisplayMember = "Value";
                        combobox.ValueMember = "Key";
                        innerPanel.Controls.Add(combobox);
                        break;
                }
                panel.Controls.Add(innerPanel);
                panel.PerformLayout();
            }
            Button closeButton = new Button();
            closeButton.Text = "Weiter";
            closeButton.Click += delegate(object buttonclick_sender, EventArgs buttonclick_e)
            {
                bool allthere = true;
                List<PersonAttribute> atts = this.currentDef.personAttributes.ToList<PersonAttribute>(); ;
                foreach (PersonAttribute att in atts)
                {
                    if (att.Mandatory)
                    {
                        Label attLabel = panel.Controls.Find("label_" + att.Varname, true)[0] as Label;
                        switch (att.Fieldtype)
                        {
                            case PAFieldtype.Textfield:
                                TextBox box = panel.Controls.Find(att.Varname, true)[0] as TextBox;
                                if (box.Text.Length == 0)
                                {
                                    attLabel.ForeColor = Color.Red;
                                    allthere = false;
                                }
                                else attLabel.ForeColor = Color.Black;
                                break;
                            case PAFieldtype.Radiobuttons:
                                Panel attPanel = panel.Controls.Find("panel_" + att.Varname, true)[0] as Panel;
                                RadioButton selectedButton = attPanel.Controls.OfType<RadioButton>().FirstOrDefault(r => r.Checked);
                                if (selectedButton == null)
                                {
                                    attLabel.ForeColor = Color.Red;
                                    allthere = false;
                                }
                                else attLabel.ForeColor = Color.Black;
                                break;
                            case PAFieldtype.Selector:
                                ComboBox combobox = panel.Controls.Find(att.Varname, true)[0] as ComboBox;
                                if (combobox.SelectedIndex == -1)
                                {
                                    attLabel.ForeColor = Color.Red;
                                    allthere = false;
                                }
                                else attLabel.ForeColor = Color.Black;
                                break;
                        }
                    }
                }
                if (!allthere) MessageBox.Show(this, "Bitte beantworten Sie alle Pflichtfragen (rote Markierung)!", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information);
                else
                {
                    //save in savedataObject
                    foreach (PersonAttribute att in atts)
                    {
                        switch (att.Fieldtype)
                        {
                            case PAFieldtype.Textfield:
                                TextBox box = panel.Controls.Find(att.Varname, true)[0] as TextBox;
                                datasaveObject.addSaveObject("Attributes", att.Varname, box.Text);
                                break;
                            case PAFieldtype.Radiobuttons:
                                Panel attPanel = panel.Controls.Find("panel_" + att.Varname, true)[0] as Panel;
                                RadioButton selectedButton = attPanel.Controls.OfType<RadioButton>().FirstOrDefault(r => r.Checked);
                                if (selectedButton != null)
                                {
                                    string[] keysplit = selectedButton.Name.Split('_');
                                    datasaveObject.addSaveObject("Attributes", att.Varname, keysplit[1]);
                                }
                                break;
                            case PAFieldtype.Selector:
                                ComboBox combobox = panel.Controls.Find(att.Varname, true)[0] as ComboBox;
                                datasaveObject.addSaveObject("Attributes", att.Varname, combobox.SelectedValue);
                                break;
                        }
                    }
                    //save to file if not preview
                    if (!this.isPreview)
                    {
                        ExcelWorksheet attSheet = this.currentExcel.Workbook.Worksheets["Attributes"];
                        if (attSheet == null)
                        {
                            attSheet = this.currentExcel.Workbook.Worksheets.Add("Attributes");
                            attSheet.Row(1).Style.Font.Bold = true;
                            attSheet.Cells[1, 1].Value = "ID";
                        }
                        int row = 2;
                        while (attSheet.Cells[row, 1].Value != null && attSheet.Cells[row, 1].Value.ToString().Length > 1) row++;
                        attSheet.Cells[row, 1].Value = this.currentSession;
                        foreach (PersonAttribute att in atts)
                        {
                            int column = 2;
                            while (attSheet.Cells[1, column].Value != null && attSheet.Cells[1, column].Value.ToString() != att.Varname) column++;
                            attSheet.Cells[1, column].Value = att.Varname;
                            switch (att.Fieldtype)
                            {
                                case PAFieldtype.Textfield:
                                    TextBox box = panel.Controls.Find(att.Varname, true)[0] as TextBox;
                                    attSheet.Cells[row, column].Value = box.Text;
                                    break;
                                case PAFieldtype.Radiobuttons:
                                    Panel attPanel = panel.Controls.Find("panel_" + att.Varname, true)[0] as Panel;
                                    RadioButton selectedButton = attPanel.Controls.OfType<RadioButton>().FirstOrDefault(r => r.Checked);
                                    if (selectedButton != null)
                                    {
                                        string[] keysplit = selectedButton.Name.Split('_');
                                        attSheet.Cells[row, column].Value = Convert.ToInt32(keysplit[1]);
                                    }
                                    break;
                                case PAFieldtype.Selector:
                                    ComboBox combobox = panel.Controls.Find(att.Varname, true)[0] as ComboBox;
                                    attSheet.Cells[row, column].Value = Convert.ToInt32(combobox.SelectedValue);
                                    break;
                            }
                        }
                    }
                    attForm.DialogResult = DialogResult.OK;
                    attForm.Close();
                }
            };
            panel.Location = new Point(10, 10);
            panel.Controls.Add(closeButton);
            attForm.Controls.Add(panel);
            attForm.ClientSize = new System.Drawing.Size(20 + panel.Width, 20 + panel.Height);
            attForm.PerformLayout();
            attForm.StartPosition = FormStartPosition.Manual;
            attForm.Location = new Point(this.Location.X + this.Width / 2 - attForm.Width / 2, this.Location.Y + this.Height / 2 - attForm.Height / 2);
            DialogResult dr = attForm.ShowDialog(this);
            if (dr == DialogResult.OK) this.mainWorker.mre.Set();
            else this.Close();
        }

        public void doSequences(int seq_offset)
        {
            if (seq_offset >= this.currentDef.sequences.Count)
            {
                this.mainWorker.mre.Set();
                return;
            }
            List<Sequence> sequences = this.currentDef.sequences.ToList<Sequence>();
            sequences.Sort();
            this.curSequence = sequences[seq_offset];
            //get events and check if entering sequence
            bool enter = true;
            string[] events = this.curSequence.Events.Split(';');
            foreach (string one in events)
            {
                string[] oneevent = one.Split('=');
                if (oneevent[0]=="entering")
                {
                    try
                    {
                        enter = (bool)(this.assUserdefFunctions.GetType("Experimentor.User_defined_functions").GetMethod(oneevent[1]).Invoke(null, new object[] { this.datasaveObject }));
                    }
                    catch (Exception ex)
                    {
                        if (this.isPreview) this.Invoke((MethodInvoker)delegate { MessageBox.Show(this,"Error:\n"+ex.Message,"Error",MessageBoxButtons.OK,MessageBoxIcon.Error); });
                    }
                }
            }
            if (enter)
            {
                //begin the sequence
                this.Controls.Clear();
                this.Controls.Add(this.curSequence.getLayout().doLayout(this.ClientRectangle, this.curSequence.LayoutArgs));
                this.PerformLayout();
                this.curSetOrder = new List<int>();
                this.curSetOrder.Add(this.curSequence.Fromset);
                for (int setnum = this.curSequence.Fromset + 1; setnum <= this.curSequence.Toset; setnum++) this.curSetOrder.Add(setnum);
                if (this.curSequence.Randomize)
                {
                    Random rnd = new Random();
                    this.curSetOrder = this.curSetOrder.OrderBy(d => rnd.NextDouble()).ToList<int>();
                }
                if (this.curSequence.hasSaveParts() && !this.isPreview && this.currentExcel.Workbook.Worksheets[curSequence.SheetName] == null)
                {
                    //prepare the Excel Sheet
                    ExcelSave.createSequenceSheet(this.currentExcel.Workbook, curSequence.SheetName);
                    List<string> cellsurfixes = new List<string>();
                    foreach (SequencePart part in this.curSequence.Parts)
                    {
                        string slidename = "";
                        int slidenum = part.Slide+1;
                        while (slidenum > 0)
                        {
                            slidename = alphabet[(slidenum - 1) % 26] + slidename;
                            slidenum = (slidenum - 1) / 26;
                        }
                        //add a cell for time if needs to be saved
                        if ((part.ShowMax > part.ShowMin || part.Probability < 1) && part.Save) cellsurfixes.Add("_Part_" + part.Order + "_Slide_" + slidename);
                        //add cells for user action if needs to be saved
                        if (part.Save) foreach (string uasurfix in part.getUserAction().Savesurfixes) cellsurfixes.Add("_Part_" + part.Order + "_Slide_" + slidename + uasurfix);
                    }
                    for (int i=curSequence.Fromset; i<=curSequence.Toset; i++)
                    {
                        foreach (string cellsurfix in cellsurfixes) ExcelSave.addToSequenceSheet(this.currentExcel.Workbook, curSequence.SheetName, "Set_" + i + cellsurfix);
                    }
                }
                this.curSequenceWorker = new SequenceWorker(this, seq_offset);
                this.curSequenceWorker.RunWorkerAsync();
            }
            else this.doSequences(++seq_offset);
        }

        public void saveSet(int set)
        {
            //add data from the set to the save object
            this.datasaveObject.addOtherDatasaveObject(this.setData);
            //write to the excel sheet if not preview
            if (!this.isPreview) this.setData.writeToExcel();
            //clear the set data
            this.setData = new DatasaveObject(this.isPreview, this.currentSession, Path.GetDirectoryName(this.currentFile), this.currentExcel);
        }

        public void doSets(int set_offset)
        {
            if (set_offset >= this.curSetOrder.Count)
            {
                //get events and check if code for ending sequence
                string[] events = this.curSequence.Events.Split(';');
                foreach (string one in events)
                {
                    string[] oneevent = one.Split('=');
                    if (oneevent[0] == "end")
                    {
                        try
                        {
                            this.assUserdefFunctions.GetType("Experimentor.User_defined_functions").GetMethod(oneevent[1]).Invoke(null, new object[] { this.datasaveObject });
                        }
                        catch (Exception ex)
                        {
                            if (this.isPreview) this.Invoke((MethodInvoker)delegate { MessageBox.Show(this, "Error:\n" + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); });
                        }
                    }
                }
                //end the sequence
                this.curSequenceWorker.mre.Set();
                return;
            }
            this.curSet = this.curSetOrder[set_offset];
            if (this.curSequence.hasSaveParts() && !this.isPreview) ExcelSave.addToSetorder(this.currentExcel.Workbook, curSequence.SheetName, this.currentSession, this.curSet);
            this.curSeqParts = this.curSequence.Parts.ToList<SequencePart>();
            this.curSeqParts.Sort();
            this.curSetWorker = new SetWorker(this, set_offset);
            this.curSetWorker.RunWorkerAsync();
        }

        public void doSlides(int seqPart_offset)
        {
            this.curSequence.getLayout().clearAllSlides();
            if (seqPart_offset >= this.curSeqParts.Count)
            {
                this.curSetWorker.mre.Set();
                return;
            }
            DataRow slides = this.currentDef.slides.Select("Order = "+this.curSet.ToString())[0];
            List<SequencePart> curParts = new List<SequencePart>();
            curParts.Add(this.curSeqParts[seqPart_offset]);
            while (seqPart_offset < this.curSeqParts.Count - 1 && this.curSeqParts[seqPart_offset].Order == this.curSeqParts[seqPart_offset + 1].Order) curParts.Add(this.curSeqParts[++seqPart_offset]);
            this.curSeqPartWorker = new SeqPartWorker(this, seqPart_offset);
            Random rnd = new Random();
            foreach (SequencePart part in curParts)
            {
                string slidename = "";
                int slidenum = part.Slide+1;
                while (slidenum > 0)
                {
                    slidename = alphabet[(slidenum - 1) % 26] + slidename;
                    slidenum = (slidenum - 1) / 26;
                }
                string tablename = curSequence.SheetName;
                string cellname = "Set_" + this.curSet + "_Part_" + part.Order + "_Slide_" + slidename;
                //check if slide should be presented
                double p = part.Probability;
                if (p < 1 && rnd.NextDouble() > p)
                {
                    if (part.Save) this.setData.addSaveObject(tablename, cellname, "-");
                    continue;
                }
                //set presentation time
                //get events and check if code calculation presentation time
                int showms=1000;
                bool calcms = false;
                if (part.Events != null)
                {
                    string[] events = part.Events.Split(';');
                    foreach (string one in events)
                    {
                        string[] oneevent = one.Split('=');
                        if (oneevent[0] == "setduration")
                        {
                            try
                            {
                                showms = Convert.ToInt32(this.assUserdefFunctions.GetType("Experimentor.User_defined_functions").GetMethod(oneevent[1]).Invoke(null, new object[] { this.datasaveObject }));
                                this.setData.addSaveObject(tablename, cellname, showms);
                                calcms = true;
                            }
                            catch (Exception ex)
                            {
                                if (this.isPreview) this.Invoke((MethodInvoker)delegate { MessageBox.Show(this, "Error:\n" + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); });
                            }
                        }
                    }
                }
                //else take it from the SequencePart definition
                if (!calcms)
                {
                    showms = part.ShowMin;
                    if (part.ShowMax > part.ShowMin)
                    {
                        if (part.ShowBalanced)
                        {
                            string timeslistname = "Sequence_" + this.curSequence.SheetName + "_Part_" + part.Order + "_Slide_" + slidename;
                            if (!this.presentationTimes.ContainsKey(timeslistname)) this.presentationTimes.Add(timeslistname, new List<int>());
                            //fill the list if empty
                            if (this.presentationTimes[timeslistname].Count == 0) for (int onetime = part.ShowMin; onetime <= part.ShowMax; onetime += part.ShowStep) this.presentationTimes[timeslistname].Add(onetime);
                            //take one presentation time at random and 
                            showms = this.presentationTimes[timeslistname].ElementAt(rnd.Next(0, this.presentationTimes[timeslistname].Count));
                            this.presentationTimes[timeslistname].Remove(showms);
                        }
                        else
                        {
                            if (part.ShowStep == 0) showms = rnd.Next(part.ShowMin, part.ShowMax + 1);
                            else showms += part.ShowStep * (rnd.Next(0, (part.ShowMax - part.ShowMin) / part.ShowStep + 1));
                        }
                        //save presentation time if needed
                        if (part.Save) this.setData.addSaveObject(tablename, cellname, showms);
                    }
                }
                //load the timer if any
                WaitWorker waitworker = null;
                if (showms > 0) waitworker = new WaitWorker(this.curSeqPartWorker, showms);
                //SHOW SLIDE
                Control slidecontrol = this.curSequence.getLayout().showSlide(this.currentFile, slides[part.Slide + 1].ToString(), part.Position);
                //run timer if any
                if (waitworker != null) waitworker.RunWorkerAsync();
                //add user action if any
                if (!(part.getUserAction() is UserAction_None))
                {
                    Control useractioncontrol = part.getUserAction().getControl(this.curSeqPartWorker, slidecontrol, (part.Save?this.setData:null), curSequence.SheetName, cellname);
                    if (useractioncontrol!=null) this.curSequence.getLayout().addUserActionControl(useractioncontrol, part.Position);
                }
            }
            this.curSeqPartWorker.RunWorkerAsync();
        }

        private void form_ExperimRun_FormClosing(object sender, FormClosingEventArgs e)
        {
            try
            {
                if (this.currentExcel != null && !this.isPreview) this.currentExcel.Save();
            } catch (InvalidOperationException ioe)
            {
                MessageBox.Show(this, "FATAL: " + ioe.Message + "\n" + ioe.InnerException.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void form_ExperimRun_KeyUp(object sender, KeyEventArgs e)
        {
            if (this.isPreview && e.KeyCode == Keys.Escape) this.Close();
        }
    }
}
